/**
 * 
 * ETH AI GAME LIBRARY
 * 
 * Author: Michael Jaworski
 * Copyright (c) Michael Jaworski 2010
 * http://code.google.com/p/eth/
 * 
 * Licence Agreement
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *	
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

package lib.eth.systems.fuzzy
{
	import lib.eth.core.interfaces.IEthProcess;
	import lib.eth.core.interfaces.IRecyclable;
	import lib.eth.systems.fuzzy.operations.FuzzyOperations;
	import lib.eth.systems.fuzzy.functions.IFuzzyFunction;
	import lib.eth.systems.process.EthProcess;

	/**
	 * a mapping of values in the range [0,1]
	 * 
	 * @note a fuzzy curve must have a minimum of two sample points (0, 1)
	 * @note the sample positions on a fuzzy curve are always evenly distributed
	 * @note a fuzzy curve is not properly composed until either procedural or heuristic is called
	 * 
	 * @version 0.1
	 */ 
	public class FuzzyCurve implements IEthProcess
	{
	
		/**
		 * all curves must have two samples (0, 1) - needed to make code
		 * simpler; and really... what's the point of a curve with one
		 * sample?!
		 */ 
		public static const MIN_SAMPLES:int = 2;
		
		/**
		 * the highest value any two values in the curve may differ
		 * used in value method to shortcut computation
		 * 
		 */ 
		protected static const TOLERANCE:Number = 0.0001;
		
		/**
		 * one less than the number of samples in the system
		 */
		private var _granularity:int;
		
		/**
		 * the space between each sample (they are evenly spaced)
		 */ 
		private var _bucketSize:Number;
		
		/**
		 * curve itself (of numbers)
		 */ 
		protected var _curve:Vector.<Number>;
		
		/**
		 * some curves may be controlled by fuzzy operators and so they are simply
		 * filled with output from operators
		 * 
		 * this is a reference back to the operator for efficient graph operations
		 * 
		 * if the curve was created with heuristic or procedural, this is
		 * null - the curve has user data
		 */
		private var _operator:FuzzyOperator;
		
		/**
		 * to create a fuzzy curve you must call procedural OR heuristic (not both)
		 * 
		 * <p>
		 * pre  : class data is null
		 * post : class data is null, bucketSize is 1
		 * </p>
		 * 
		 */ 
		public function FuzzyCurve ()
		{			
			_granularity = 0;
			_bucketSize = 1;		// so we do not divide by zero
			_curve = null;			// 
			_operator = null;
		}
		
		/**
		 * create a FuzzyCurve with a Fuzzy Function
		 * 
		 * @param samplesIn the number of samples (evenly spaced) in the curve
		 * @param ffIn the function to use to generate values for the curve
		 * 
		 * <p>
		 * pre  : curve is null
		 * post : curve is not null, and has <samples> values by the fuzzy function provided
		 * </p>
		 * 
		 */ 
		public function procedural (samplesIn:int, ffIn:IFuzzyFunction) : void
		{
			// number of 
			var samples:int = samplesIn;
			
			// there needs to be two samples minimum (0, 1)
			if (samples < MIN_SAMPLES)
				return;
			
			// calculate profile of curve
			_granularity = samples - 1;
			_bucketSize = 1.0 / _granularity;
			
			// create curve at level of granularity
			_curve = new Vector.<Number> (samples, true);
				
			// initialize with data
			for (var i:int = 0; i < samples; i++)
				_curve[i] = ffIn.sample (i * _bucketSize);
		}
		
		/**
		 * create a FuzzyCurve with a heuristic mapping
		 * 
		 * @param mappingIn an array containing the curve values, must have length > 2
		 * 
		 * <p>
		 * pre  : curve is null
		 * post : the curve is not null, and has array.length samples by the array provided
		 * </p>
		 *  
		 */ 
		public function heuristic (mappingIn:Array) : void
		{
			var samples:int = mappingIn.length;
			
			// there needs to be two samples minimum (0, 1)
			if (samples < MIN_SAMPLES)
				return;
				
			// calculate profile of curve
			_granularity = samples - 1;
			_bucketSize = 1.0 / _granularity;
			
			// create curve at level of granularity
			_curve = new Vector.<Number> (samples, true);
			
			// initialize with data
			for (var i:int = 0; i < samples; i++)
				_curve[i] = mappingIn [i];
		}
	
		/**
		 * set the curve so it can be controlled by an operator. we create the curve
		 * and assign all samples to 0.0
		 * 
		 * <p>
		 * pre  : if the curve exists, it is wiped, samplesIn >= MIN_SAMPLES
		 * post : the curve is not null, and has samples set by <samples>
		 * </p>
		 * 
		 */
		internal function controlled (operatorIn:FuzzyOperator, samplesIn:int) : void
		{
			// there needs to be two samples minimum (0, 1)
			if (samplesIn < MIN_SAMPLES)
				return;			

			// create tie back to operator
			_operator = operatorIn;
			
			// calculate profile of curve
			_granularity = samplesIn - 1;
			_bucketSize = 1.0 / _granularity;		
			
			// create curve at level of granularity
			_curve = null;
			_curve = new Vector.<Number> (samplesIn, true);			
			
			// initialize with data
			for (var i:int = 0; i < samplesIn; i++)
				_curve[i] = 0.0;			
		}
		
		
		/**
		 * super-fast getter for the curve
		 * 
		 * @note use if you are applying two curves that have the same granularity
		 * 
		 */ 
		internal function getIndex (xIndexIn:int) : Number
		{
			return _curve[xIndexIn];
		}
		
		/**
		 * super-fast setter for the curve
		 * 
		 * @note use if you are applying two curves that have the same granularity
		 * 
		 */ 
		internal function setIndex (xIndexIn:int, valueIn:Number) : void
		{
			// set the curve
			_curve[xIndexIn] = valueIn;
		}		
		
		/**
		 * returns a value on the curve 
		 * 
		 * @param xPositionIn the position (0-1) on the x-axis of the curve
		 * 
		 * @return the value on the curve above the position. uses, linear interpolation
		 * 
		 */ 
		public function getValue (xPositionIn:Number) : Number
		{
			// get array index
			var lowerIndex:Number = (int)(xPositionIn / _bucketSize);
			
			// if top most value, then just return that (1)
			if (lowerIndex >= _granularity)
				return _curve[_granularity];

			var difference:Number = xPositionIn - (lowerIndex * _bucketSize); 
			
			// shortcut calculations if close enough
			if (difference < TOLERANCE)
				return _curve[lowerIndex];
			
			// how close xPositionX is to the lowerIndex
			var percentBetweenIndices:Number = difference / _bucketSize;
			
			// calculate between value
			return (_curve[lowerIndex] * (1 - percentBetweenIndices)) + (_curve[lowerIndex + 1] * percentBetweenIndices);
		}
		
		/**
		 * setter for the curve, you can set at any point. If the xPositionIn is 
		 * the same as a sample point then curve value is set to the valueIn.
		 * If the xPositionIn is at a point between two sample points then the 
		 * closest sample point value is adjusted towards the valueIn by the 
		 * percentage of how close the xPositionIn is to that closest sample point
		 * 
		 * @param xPositionIn the position we should apply the valueIn to
		 * 
		 * @param valueIn the value used to adjust the curve, application is limited
		 * by closeness of xPositionIn to a sample point, the closer xPositionIn to
		 * a sample point, then the closer the impact of the valueIn on a the curve
		 * 
		 */ 
		public function setValue (xPositionIn:Number, valueIn:Number) : void
		{
			// get lower array index
			var lowerIndex:int = (int)(xPositionIn / _bucketSize);
			
			// how close xPositionX is to the lowerIndex
			var percentBetweenIndices:Number = (xPositionIn - (_bucketSize * lowerIndex)) / _bucketSize;
			
			// index closest to xPositionIn (the one we should update)
			var closestIndex:int = lowerIndex;
			
			// if xPositionIn is closer to higher value
			if (percentBetweenIndices > 0.5)
			{
				// set the index up 1
				closestIndex = lowerIndex + 1;
			}
			else
			{
				// flip the percentage so we move towards the lower value
				percentBetweenIndices = 1 - percentBetweenIndices;
			}
			
			// bring the appropriate index closer to the value set, by percentage away			
			// brings the value up by the difference with the percentage applied towards the new value
			_curve[closestIndex] += ((valueIn - _curve[closestIndex]) * percentBetweenIndices);
		}
		
		/**
		 * @return the number of value points in the curve
		 */ 
		public function get samples () : int
		{
			// if you want to use a controlled result, then it will not have a curve
			// when you set it as an operand, but the samples must be some number, so
			// make it zero so the other operand sets the size and then this will
			// be set with zero values
			if (_curve === null)
				return 0;
			
			return _curve.length;
		}
		
		/**
		 * @return the number of value points (samples) in the curve, minus one
		 */ 
		public function get granularity () : int
		{
			return _granularity;
		}
		
		/**
		 * @return the space between two samples (same for each pair of samples) - constant
		 */
		public function get sampleSize () : Number
		{
			return _bucketSize;
		}
		
		/**
		 * @return true if the curve is being controlled by a FuzzyOperator
		 */ 
		public function get isControlled () : Boolean
		{ 
			return (_operator !== null);
		}
		
		/**
		 * @return the operator controlling the values of this fuzzy curve
		 */ 
		public function get operator () : FuzzyOperator
		{
			return _operator;
		}
		
		/**
		 * 
		 * if controlled, the curve will ripple updates backwards
		 * until all non-controlled curves are drawn through
		 * operators to provide the most recent calculation
		 *  
		 * if not controlled, this method will do nothing. The
		 * operator called from other controlled curves
		 * will draw data from it
		 *  
		 */
		public function update () : void
		{								
			if (isControlled)
			{
				// recursively call updates on operands
				_operator.operandOne.update ();
				_operator.operandTwo.update ();
				
				// when operands are updated, then recalculate with operator
				_operator.update ();
			}
		}	
	}
}